package classificator.tree;

import classificator.data.Attribute;
import classificator.data.ContinuousAttribute;
import classificator.data.Data;
import classificator.data.DiscreteAttribute;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.TreeSet;

/**Modella l'entità albero di decisione come insieme di sotto-alberi 
 *
 * @author 467644
 */
public class DecisionTree implements Serializable{
    //Radice del sotto-albero corrente 
    private Node root;
    //Array di sotto-alberi originanti nel nodo root. Esiste un array per ogni livello di profondità nell'intero albero 
    private DecisionTree childTree[];
    
    /**Costruttore. Istanzia un sotto-albero dell'intero albero 
     * 
     */
    public DecisionTree() {
        //TODO
    }
    
    /**Costruttore. Istanzia un sotto-albero dell'intero albero e avvia l'induzione 
     * dell'albero dagli esempi di training in input 
     * 
     * @param trainingSet training set complessivo 
     */
    public DecisionTree(Data trainingSet){
        learnTree (trainingSet, 0, trainingSet.getNumberOfExamples()-1, 
                    trainingSet.getNumberOfExamples()*10/100);
    }

    /**restituisce la radice dell'albero
     * 
     * @return radice
     */
    
    public Node getRoot() {
        return root;
    }
    
    /**
     * 
     * @param child
     * @return 
     */
    public DecisionTree subTree(int child){
        return childTree[child];
    }
    
    public void salva(String nomeFile) throws FileNotFoundException, IOException{
        FileOutputStream outFile = new FileOutputStream(nomeFile);
        ObjectOutputStream outStream = new ObjectOutputStream(outFile);
        outStream.writeObject(this);
    }
    
    public static DecisionTree carica(String nomeFile) throws FileNotFoundException, IOException, ClassNotFoundException{
        FileInputStream inFile = new FileInputStream(nomeFile);
        ObjectInputStream inStream = new ObjectInputStream(inFile);
        return (DecisionTree) inStream.readObject();
    }
    
    /**verifica se il sotto-insieme corrente può essere coperto da un nodo foglia 
     * controllando la cardinalità di tale sotto-insieme (inferiore al numero  minino) 
     * e la frequenza dei valori dell'attributo di classe. 
     * 
     * @param trainingSet training set complessivo
     * @param begin indice iniziale del sotto-insieme di training
     * @param end indice finale del sotto-insieme di training
     * @param numberOfExamplesPerLeaf numero minimo di esempi che devono ricadere in una foglia. 
     * @return esito sulle condizioni per i nodi fogliari 
     */
    private boolean isLeaf(Data trainingSet,int begin, int end,int numberOfExamplesPerLeaf){
        //Il nodo corrente è foglia se:
        //1) il numero di esempi addestramento che ricadono nella partizione corrente è minore di numberOfExamplesPerLeaf, oppure
        //2) tutti gli esempi addestramento che ricadono nella partizione corrente appartengono alla stessa classe
        if (end - begin + 1 < numberOfExamplesPerLeaf)
            return true;
        else {
            String currValue = trainingSet.getClassValue(begin);
            for (int i = begin + 1; i <= end; i++)
                if (!currValue.equals(trainingSet.getClassValue(i)))
                    return false;
            return true;
        }
    }
          
    /**istanzia un DiscreteAttribute su ciascun attributo indipendente e ne computa l'information gain:  
     * il nodo con maggior information gain tra quelli istanziati viene restituito. 
     * 
     * @param trainingSet training set complessivo
     * @param begin indice iniziale del sotto-insieme di training
     * @param end indice finale del sotto-insieme di training
     * @return nodo di split migliore per il sotto-insieme di training 
     */
    private SplitNode determineBestSplitNode(Data trainingSet, int begin, int end) throws NoSplitException{
        //System.out.println("determineBestSplitNode " + begin +"-" + end+ "-"+trainingSet.getClassAttribute());
        SplitNode bestNode = null;
        TreeSet<SplitNode> set = new TreeSet<SplitNode>();
        //per ciascuna variabile discreta si costruisce l'istanza di DiscreteNode per tale variabile 
        //e la si assegna a currentNode aggiornando bestNode in modo da mantenere 
        //il nodo che massimizza Information Gain
        for (int i = 0; i < trainingSet.getNumberOfExplanatoryAttributes(); i++){
            Attribute attr = trainingSet.getExplanatoryAttribute(i);
            if (attr instanceof DiscreteAttribute)
                set.add(new DiscreteNode(trainingSet, begin, end, (DiscreteAttribute)attr));
            else if (attr instanceof ContinuousAttribute)
                set.add(new ContinuousNode(trainingSet, begin, end, (ContinuousAttribute)attr));
        }
        //bestNode = new DiscreteNode(trainingSet, begin, end, (DiscreteAttribute)maxNode.getAttribute());
        bestNode = set.first();
        if (bestNode==null)
            throw new NoSplitException("Impossibile determinare un nodo best split");
        // applico l'ordinamento coerente con il tipo di split scelto
        trainingSet.sort(bestNode.getAttribute(), begin, end);
        return bestNode; //TODO Comparatore?
    }

    /**Genera un sotto-albero con il sotto-insieme di input istanziando un nodo fogliare
     * (isLeaf()=true) o un nodo di split. 
     * In tal caso determina il miglior nodo rispetto al sotto-insieme di input 
     * (determineBestSplitNode()), ed a tale nodo esso associa un sotto-albero 
     * avente radice il nodo medesimo (root) e avente un numero di rami pari il 
     * numero dei figli determinati dallo split (childTree[]).     
     * Ricorsivamente, per  ogni oggetto  DecisionTree in childTree[] sarà invocato il 
     * metodo learnTree() per l'apprendimento su un insieme ridotto del sotto-insieme 
     * di addestramento attuale (begin... end). 
     * Nella condizione in cui il nodo di split non origina figli, il nodo diventa fogliare. 
     *  
     * @param trainingSet training set complessivo
     * @param begin indice iniziale del sotto-insieme di training
     * @param end indice finale del sotto-insieme di training
     * @param numberOfExamplesPerLeaf numero massimo di esempi che una foglia deve contenere 
     */
    private void learnTree(Data trainingSet,int begin, int end,int numberOfExamplesPerLeaf){
        if (isLeaf(trainingSet, begin, end, numberOfExamplesPerLeaf))
            //determina la classe che compare più frequentemente nella partizione corrente
            root = new LeafNode(trainingSet, begin, end);
        else {
            //split node
            try{
                root = determineBestSplitNode(trainingSet, begin, end);
                if(root.getNumberOfChildren()>1){
                    childTree = new DecisionTree[root.getNumberOfChildren()];
                    for(int i=0;i<root.getNumberOfChildren();i++){
                        childTree[i] = new DecisionTree();
                        childTree[i].learnTree(trainingSet, ((SplitNode)root).getSplitInfo(i).beginIndex, 
                                ((SplitNode)root).getSplitInfo(i).endIndex, numberOfExamplesPerLeaf);
                    }
                } else //figli = 1
                    root = new LeafNode(trainingSet,begin,end);
            } catch (NoSplitException nse){
                System.err.println(nse.getMessage());
            }
            
        }
    }

    /**Concatena in una String tutte le informazioni di root-childTree[] correnti 
     * invocando i relativi metodo  toString(): nel caso il root corrente è di split 
     * vengono concatenate anche le informazioni dei rami. 
     * 
     * @return oggetto String con le informazioni dell'intero albero 
     */
    @Override
    public String toString(){
        String tree = root.toString()+"\n";
        //System.out.println(root.getClass().getName());
        if( root instanceof LeafNode){
            //non deve fare nulla
        } else { //può essere un nodo di split o discreto
            //split node
            for(int i=0;i<childTree.length;i++)
                tree += childTree[i];
        }
        return tree;
    }

    /**Scandisce ciascun ramo dell'albero completo dalla radice alla  foglia concatenando 
     * le informazioni dei nodi di split fino al nodo foglia. 
     * In particolare per ogni sotto-albero (oggetto DecisionTree) in childTree[] 
     * concatena le informazioni del nodo root: se è di  split discende ricorsivamente     
     * l'albero per ottenere le informazioni del nodo sottostante (necessario per ricostruire 
     * le condizioni in AND) d'ogni ramo-regola, se è di foglia (leaf) termina 
     * l'attraversamento visualizzando la regola. 
     */
    public void printRules(){
        String current = "";
        System.out.println("********* RULES **********");
        if (root instanceof LeafNode){
            current = "CLASS= " + ((LeafNode) root).getPredictedClassValue();
            System.out.println(current);
        }  else {
            for (int i = 0; i < childTree.length; i++){
                current = ((SplitNode) root).getAttribute().getName() + 
                        ((SplitNode) root).getSplitInfo(i).getComparator() + 
                        ((DiscreteNode) root).getSplitInfo(i).getSplitValue();
                childTree[i].printRules(current);
            }
        }
        System.out.println("*************************\n");
    }

    /**Concatena alle informazioni in current del precedente nodo quelle del nodo root 
     * del corrente sotto-albero (oggetto DecisionTree): se il nodo corrente è di split 
     * il metodo viene invocato ricorsivamente con current e le informazioni del nodo 
     * corrente, se è fogliare (leaf) visualizza tutte le informazioni concatenate.
     * 
     * @param current Informazioni del nodo di split del sotto-albero al livello superiore 
     */
    private void printRules(String current){
        //Supporta il metodo public void printRules()
        String s = current;
        if (root instanceof LeafNode)
            System.out.println("if (" + current + ") then predicted value is " + 
                    ((LeafNode) root).getPredictedClassValue());
        else {
            for (int i = 0; i < childTree.length; i++){
                current += " AND ";
                current += ((SplitNode) root).getAttribute().getName() + 
                        ((SplitNode) root).getSplitInfo(i).getComparator() + 
                        ((SplitNode) root).getSplitInfo(i).getSplitValue();
                childTree[i].printRules(current);
                current = s;
            }
        }
    }
}